<?php
/**
 * UI控件抽象类类
 * 
 * 标签生成参考：
 *  http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_XHTML-1.0-Strict
 *
 * @since 1.1.0
 * @package if
 * @subpackage module.ui
 */
abstract class UIControl extends IObject {
	private $attrs = array();
	private $options = array();
	private $children = array();
	private $parent;
	protected $tpl;
	protected $tplExt = "tpl";
	protected $reserveAttrs = array();
	private $name;
	/**
	 * Enter description here...
	 *
	 * @var Smarty
	 */
	protected $smarty;
	
	/**
	 * Enter description here...
	 *
	 * @var IController
	 */
	protected $controller;
	const OPTION_KEYWORD = "control";
	private $content;
	private $tplVars = array();
	private $prefix;
	private $tagName;
	private static $scripts = array();
	private static $scriptLibs = array();
	
	/**
	 * 构造器
	 *
	 * @param array $options 参数，在标签中使用 control.optionName=optionValue指定
	 * @since 1.1.0
	 */
	function __construct($options = array()) {
		$this->options = $options;
	}
	
	/**
	 * 取得含有当前控件的控件
	 * 
	 * 如果指定了父控件的名字$name，那么就会查找是该名字的控件或者继承自该名字对应的控件类型的控件，比如
	 * jq:form 继承自 f:form ，那么你在查找 f:form 时，如果先遇到 jq:form ，就会立即返回 jq:form
	 *
	 * @return UIControl
	 * @since 1.1.0
	 */
	function parent($name = null) {
		if (!$name) {
			return $this->parent;
		}
		$current = $this;
		$parent = null;
		$parentClass = self::classByName($name);
		while ($parent = $current->parent()) {
			$current = $parent;
			if ($parent->name() == $name || if_is_instance_of($parent, $parentClass)) {
				return $parent;
			}
		}
		return;
	}
	
	/**
	 * 取得当前控件下的所有子控件，只有在
	 * - _before_render
	 * - _after_render
	 * 中才能获得，而且此时对子控件的任何设置都是无效的
	 *
	 * @return array
	 * @since 1.1.0
	 */
	function children() {
		return $this->children;
	}
	
	/**
	 * 设置当前控件的父控件
	 *
	 * @param UIControl $parent 父控件
	 * @since 1.1.0
	 */
	function setParent(UIControl $parent) {
		$this->parent = $parent;
	}
	
	/**
	 * 为当前控件添加子控件
	 *
	 * @param UIControl $child 子控件
	 * @since 1.1.0
	 */
	function addChild(UIControl $child) {
		$child->setParent($this);
		$this->children[$child->id()] = $child;
	}
	
	/**
	 * 设置参数
	 *
	 * @param string|array $name 参数名或者参数集
	 * @param mixed $value 值
	 * @since 1.1.0
	 */
	function setAttr($name, $value = null) {
		if (is_array($name)) {
			foreach ($name as $_name => $_value) {
				$this->attrs = if_array_set($this->attrs, $_name, $_value);
			}
		}
		else {
			$this->attrs = if_array_set($this->attrs, $name, $value);
		}
	}
	
	/**
	 * 取得控件属性值
	 *
	 * @param string $name 控件属性名
	 * @return mixed
	 * @since 1.1.0
	 */
	function attr($name) {
		return if_array_get($this->attrs, $name);
	}
	
	/**
	 * 判断某个属性是否被定义
	 *
	 * @param string $name 属性名
	 * @return boolean
	 * @since 1.1.2
	 */
	function hasAttr($name) {
		return !is_null(if_array_get($this->attrs, $name));
	}
	
	/**
	 * 取得控件所有属性名值对
	 *
	 * @return array
	 * @since 1.1.0
	 */
	function attrs() {
		return $this->attrs;
	}
	
	/**
	 * 设置控件名
	 * 
	 * 如 f:form
	 *
	 * @param string $name 控件名
	 * @since 1.1.0
	 */
	function setName($name) {
		$this->name = $name;
		list($this->prefix, $this->tagName) = explode(":", $this->name, 2);
	}
	
	/**
	 * 取得控件名
	 *
	 * @return string
	 * @since 1.1.0
	 */
	function name() {
		return $this->name;
	}
	
	/**
	 * 取得当前控件名的前缀
	 * 
	 * 如 <f:form/> 的前缀是 f
	 *
	 * @return string
	 * @since 1.1.0
	 */
	function prefix() {
		return $this->prefix;
	}
	
	/**
	 * 取得当前控件的标签名
	 * 
	 * 如 <f:form/> 的标签名是 form
	 *
	 * @return string
	 * @since 1.1.0
	 */
	function tagName() {
		return $this->tagName;
	}
	
	/**
	 * 取得控件ID
	 * 
	 * 可以在控件中使用 control.id 指定此值，如：
	 * - <f:form control.id="my_form"/>
	 * 
	 * 如果没有指定此值，则自动生成一个唯一的标识
	 * 
	 * @return string
	 * @since 1.1.0
	 */
	function id() {
		return $this->options["id"];
	}
	
	/**
	 * 设置当前的控制器
	 *
	 * @param IController $controller 控制器
	 * @since 1.1.0
	 */
	function setController($controller) {
		$this->controller = $controller;
	}
	
	/**
	 * 取得当前控件的选项
	 *
	 * @return array
	 * @since 1.1.0
	 */
	function options() {
		return $this->options;
	}
	
	/**
	 * 获取选项的值
	 *
	 * @param string $key 选项名
	 * @return mixed
	 * @since 1.1.0
	 */
	function option($key) {
		return if_array_get($this->options, $key);
	}
	
	/**
	 * 设置当前控件的选项
	 *
	 * @param array $options 选项
	 * @since 1.1.0
	 */
	function setOptions(array $options) {
		$this->options = $options;
	}
	
	/**
	 * 为控件添加一个选项
	 *
	 * @param string $name 选项名
	 * @param mixed $value 选项值
	 * @since 1.1.0
	 */
	function addOption($name, $value) {
		$this->options[$name] = $value;
	}
	
	/**
	 * 设置控件所包含的内容
	 * 
	 * 如：<f:form>This is content.</f:form>
	 *
	 * @param string $content 内容
	 * @since 1.1.0
	 */
	function setContent($content) {
		$this->content = $content;
	}
	
	/**
	 * 取得控件所包含的内容
	 *
	 * @return string
	 * @since 1.1.0
	 */
	function content() {
		return $this->content;
	}
	
	/**
	 * 为控件模板增加一个新的变量
	 * 
	 * 如果当前控件没有使用模板来绘制的话，就无需设置此值
	 *
	 * @param string $name 变量名
	 * @param mixed $value 变量值
	 * @since 1.1.0
	 */
	function addTplVar($name, $value = null) {
		if (is_array($name)) {
			$this->tplVars = array_merge($this->tplVars, $name);
		}
		else {
			$this->tplVars[$name] = $value;
		}
	}
	
	
	/**
	 * 控件分析之后回调函数
	 * 
	 * 此事件在父控件_after_parse之后,_before_render之前
	 * 
	 * 此事件在子控件_before_parse之前
	 * 
	 * 此时能使用
	 * - 当前控件属性
	 * - 当前控件ID
	 * - 获取当前控件的父控件属性
	 * - 设置当前控件的父控件属性
	 * 
	 * @since 1.1.0
	 */
	function _after_parse() {}	
	
	/**
	 * 控件绘制之前回调函数
	 * 
	 * 此事件在父控件_after_parse之后,_before_render之前
	 * 
	 * 此事件在子控件_after_render之后
	 * 
	 * 此时能使用:
	 * - 当前控件属性
	 * - 当前控件ID
	 * - 当前控件包含的内容
	 * - 获取当前控件的父控件属性
	 * - 设置当前控件的父控件属性
	 * 
	 * @since 1.1.0
	 */
	function _before_render() {}
	
	/**
	 * 控件绘制之前回调函数
	 * 
	 * 此事件在父控件_after_parse之后,_before_render之前
	 * 
	 * 此事件在子控件_after_render之后
	 * 
	 * 此时能使用:
	 * - 当前控件属性
	 * - 当前控件ID
	 * - 当前控件包含的内容
	 * - 获取当前控件的父控件属性
	 * - 设置当前控件的父控件属性
	 * 
	 * @since 1.1.0
	 */	
	function _after_render() {}
	
	/**
	 * 将此控件加入到容器中（容器类为UIContainer）
	 *
	 * @since 1.1.0
	 */
	function addToContainer() {
		$container = UIContainer::instance();
		$control = $container->get($this->id());
		if ($control && $control !== $this) {
			throw new UIException("control id '" . $this->id() . "' is already be used by control '" . $control->name() . "'", 0x100096);
		}
		$container->add($this);
	}
	
	/**
	 * 添加保留的属性
	 * 
	 * 这些保留的属性将不出现在模板变量$attrs和$attrsLabel中
	 *
	 * @param string|array $attr 一个或多个属性名
	 * @since 1.1.0
	 */
	function addReserveAttrs($attr) {
		if (!is_array($attr)) {
			$attr = preg_split("/\s*,\s*/", $attr);
		}
		$this->reserveAttrs = array_merge($this->reserveAttrs, $attr);
	}
	
	/**
	 * 绘制属性
	 * 
	 * 可以使用 addReserveAttrs($attr) 方法将不需要绘制的属性剔除。
	 *
	 * @return string
	 * @since 1.1.0
	 * @see addReserveAttrs
	 */
	function renderAttrs() {
		$label = array();
		foreach ($this->attrs as $name => $value) {
			if (!in_array($name, $this->reserveAttrs) && $name != self::OPTION_KEYWORD) {
				$label[] = $name . "=\"" . htmlspecialchars(strval($value)) . "\"";
			}
		}
		return empty($label) ? "" : " " . implode(" ", $label);
	}
	
	/**
	 * 根据标签名绘制标签
	 * 
	 * 将标签绘制分发到 "_render+标签名" 的方法上，比如
	 * - <h:link/> 和 _renderLink 对应
	 * - <h:uri/> 和 _renderUri 对应
	 *
	 * @since 1.1.0
	 */
	function renderByTagName() {
		$method = "_render" . $this->tagName();
		if (!method_exists($this, $method)) {
			throw new UIException("UI control &lt;{$this->name}/&gt; is undefined", 0x100097);
		}
		return $this->$method();
	}
	
	/**
	 * 返回绘制的控件的文本内容
	 *
	 * @return string
	 * @since 1.1.0
	 */
	function render() {		
		if (!if_is_absolute_path($this->tpl)) {
			$this->tpl = dirname(__FILE__) . DIRECTORY_SEPARATOR . "template" . DIRECTORY_SEPARATOR . $this->tpl;
		}
		$tplFilename = $this->tpl . "." . $this->tplExt;
		$file = f($tplFilename);
		if (!$file->isFile()) {
			throw new UIException("can't find tpl file '{$this->tpl}' for {$this->name}", 0x100098);
		}
		
		$contents = file_get_contents($tplFilename);
		$attrs = array();
		foreach ($this->attrs as $name => $value) {
			if (!in_array($name, $this->reserveAttrs) && $name != self::OPTION_KEYWORD) {
				$attrs[$name] = $value;
			}
		}
		
		//显示
		$smarty = clone $this->smarty;
		$smarty->left_delimiter = "{{";
		$smarty->right_delimiter = "}}";
		
		//清空filter
		if (isset($smarty->_plugins["prefilter"])) {
			$smarty->_plugins["prefilter"] = array();
		}
		if (isset($smarty->_plugins["postfilter"])) {
			$smarty->_plugins["postfilter"] = array();
		}
		if (isset($smarty->_plugins["outputfilter"])) {
			$smarty->_plugins["outputfilter"] = array();
		}
	
		//关于组件
		$this->addTplVar(array(
			"control" => $this,
			"attrs" => $attrs,
			"attrsLabel" => $this->renderAttrs()
		));
		$smarty->assign($this->tplVars);

		ob_start();
		$smarty->display($this->tpl . "." . $this->tplExt);
		$contents = ob_get_clean();
		
		if ($this->option("debug") || $this->parent->option("debug")) {
			$contents = htmlspecialchars($contents);
		}
		return $contents;
	}
	
	/**
	 * 设置当前使用的Smarty对象
	 *
	 * @param Smarty $smarty Smarty对象
	 * @since 1.1.0
	 */
	function setSmarty(Smarty &$smarty) {
		$this->smarty = &$smarty;
	}
	
	/**
	 * 处理布局
	 *
	 * @param string|array $list 一个或一组数据
	 * @return string
	 * @since 1.1.0
	 */
	function processLayout($list) {
		if (!$this->attr("layout.tag")) {
			$this->setAttr("layout.tag", is_array($list) ? "ul" : "span");
		}
		$layout = $this->attr("layout");
		$ret = null;
		if (is_array($layout)) {
			if (!isset($layout["tag"])) {
				$layout["tag"] = "ul";
			}
			
			//其他属性
			$layoutAttrs = array();
			foreach ($layout as $attr => $value) {
				if ($attr != "tag") {
					$layoutAttrs[] = $attr . "=\"" . htmlspecialchars($value) . "\""; 
				}
			}
			$layoutAttrs = empty($layoutAttrs) ? "" : " " . implode(" ", $layoutAttrs);
			
			$ret .= "<" . $layout["tag"] . $layoutAttrs . ">";
			if (is_array($list)) {//一组数据
				$ret .= "\n";
				foreach ($list as $v) {
					$ret .= "  ";
					switch (strtolower($layout["tag"])) {
						case "table": 
							$ret .= "<tr>\n  <td>" . $v . "</td>\n</tr>\n";
							break;
						case "ul":
						case "ol":
							$ret .= "<li>" . $v . "</li>\n";
							break;
						case "dl":
							$ret .= "<dt>" . $v . "</dt>\n";
							break;
						case "p":
							$ret .= "<span>" . $v . "</span>\n";
							break;
						default:
							$ret .= "<" . $layout["tag"] . ">" . $v . "</" . $layout["tag"] . ">\n";
					}
				}
			}
			else {//一个数据
				$ret .= $list;
			}
			$ret .= "</" . $layout["tag"] . ">";
		}
		return $ret;
	}

	/**
	 * 使用控件名查找对应的类
	 *
	 * @param string $name 控件名
	 * @return string|null
	 * @since 1.1.0
	 */
	static function classByName($name) {
		$class = o("@ui.controls.{$name}");
		if ($class) {
			return $class;
		}
		$class = o("@ui.ext.{$name}");
		if ($class) {
			return $class;
		}
		list($prefix, $_name) = explode(":", $name, 2);
		$class = o("@ui.controls.{$prefix}:*");
		if ($class) {
			return $class;
		}
		$class = o("@ui.ext.{$prefix}:*");
		if ($class) {
			return $class;
		}
		return null;
	}
	
	/**
	 * 设置控件的ID
	 * 
	 * @param string $id ID字符串
	 * @since 1.1.0
	 */
	function setId($id) {
		$this->addOption("id", $id);
	}
	
	/**
	 * 转义一个值
	 * 
	 * @param string $value 值
	 * @param string $type 转义方式，可以选择的有 html 和 url
	 * @return string
	 * @since 1.1.0
	 */
	function escape($value, $type = "html") {
		switch ($type) {
			case "html": return htmlspecialchars($value);
			case "url": return urlencode($value);
		}
		return $value;
	}
	
	/**
	 * 添加一段脚本到客户端
	 * 
	 * 需要在客户端使用 <j:placeholder/> 指定放置该脚本的位置
	 * 
	 * @param string $script 脚本
	 * @since 1.1.0
	 */
	function addScript($script) {
		if (!in_array($script, self::$scripts)) {
			self::$scripts[] = $script;
		}
	}
	
	/**
	 * 添加一个脚本库到客户端
	 * 
	 * 需要在客户端使用 <j:placeholder/> 指定放置该脚本库的位置
	 * 
	 * @param string $lib 脚本库名，可以在_ui.php中配置
	 * @since 1.1.0
	 */	
	function addScriptLib($lib) {
		if (!is_array($lib)) {
			$lib = preg_split("/\s*,\s*/", $lib);
		}
		self::$scriptLibs = array_merge(self::$scriptLibs, $lib);
	}
	
	/**
	 * 取得所有的脚本
	 * 
	 * 重复的脚本将被剔除
	 *
	 * @return array
	 * @since 1.1.0
	 */
	static function scripts() {
		return array_unique(self::$scripts);
	}
	
	/**
	 * 取得所有的脚本库
	 * 
	 * 重复的脚本库将被剔除
	 *
	 * @return array
	 * @since 1.1.0
	 */
	static function scriptsLibs() {
		return array_unique(self::$scriptLibs);
	}
	
	/**
	 * 判断某个属性值是否为真
	 *
	 * @param string $command 属性值
	 * @param boolean $default 如果没有设置该属性时的默认值
	 * @return boolean
	 * @since 1.1.0
	 */
	function can($command, $default = true) {
		$attr = $this->attr($command);
		if (!is_null($attr)) {
			return $attr;
		}
		return $default;
	}
	
	/**
	 * 判断当前控件是否为某一个类型或其衍生类型的控件
	 *
	 * @param string $name 控件名
	 * @return boolean
	 * @since 1.1.0
	 */
	function is($name) {
		return if_is_instance_of($this, UIControl::classByName($name));
	}
}

/**
 * 过滤模板中的占位标签
 *
 * @param string $source 编译后的模板代码
 * @param Smarty $smarty Smarty对象
 * @return string
 * @since 1.1.0
 */
function ui_smarty_outputfilter($source, &$smarty) {
	$layout = if_smarty_get_var($smarty, "layout");
	if ($layout && is_file($layout)) {
		$smarty->assign("yield", $source);//兼容1.0.3以前版本
		if_smarty_set_var($smarty, "layout", null);
		$smarty->display($layout);
		return;
	}
	
	$content = "";	
	//lib
	$libs = o("@ui.javascript.libs");
	foreach (UIControl::scriptsLibs() as $lib) {
		if (isset($libs[$lib])) {
			$js = $libs[$lib];
			if (is_array($js)) {
				foreach ($js as $_js) {
					$file = __BASE__ . "/" . o("@ui.javascript.basedir") . "/" . $_js;
					$content .= "<script type=\"text/javascript\" src=\"{$file}\"></script>\n";
				}
			}
			else {
				$file = __BASE__ . "/" . o("@ui.javascript.basedir") . "/" . $js;
				$content .= "<script type=\"text/javascript\" src=\"{$file}\"></script>\n";
			}
		}
	}
	//scripts
	$scripts = UIControl::scripts();
	if (!empty($scripts)) {
		$content .= "<script type=\"text/javascript\">\n";
		$content .= "/*<![CDATA[*/\n";
		$content .= implode("\n\n", $scripts) . "\n";
		$content .= "/*]]>*/\n";
		$content .= "</script>";
	}
	if ($content) {
		$content .= "\n";
	}
	$source = str_replace('$$$$javascript_placeholder$$$$', $content, $source);
	return $source;
}

?>